<template>
  <view class="upload" v-if="showUploadList">
    <view class="empty-block" v-if="disabled && (!modelValue || modelValue.length == 0)">
      <ve-image src="empty" class="empty_img" width="64" height="64" />
      <p class="empty_text">暂无数据</p>
    </view>
    <view class="list-item list-item-image preview-wrap" v-for="(item, index) in lists" :key="index">
      <!-- 非ota订单照片，显示其它操作 -->
      <block v-if="!item.ota">
        <view v-if="deletable && !disabled" class="delete-icon" @tap.stop="deleteItem(index)">
          <!-- <ve-icon class="icon-clear-img" name="icon-clear" size="48" /> -->
          <image class="icon-clear-img" src="../../../static/icons/img-delete.png" mode="widthFix"></image>
        </view>
        <ve-line-progress
          v-if="showProgress && item.progress > 0 && item.progress != 100 && !item.error"
          :show-percent="false"
          height="16"
          class="progress"
          :percent="item.progress"
        ></ve-line-progress>
        <view @tap.stop="retry(index)" v-if="item.error" class="error-btn">点击重试</view>
      </block>
      <block v-if="!item.isImage">
        <!-- 未加载图片，不绑定 load error 事件 -->
        <image
          v-if="item.waitFetching"
          class="preview-image"
          @tap.stop="doPreviewImage(item.url || item.path || item['data-url'], index)"
          :src="item.url || item.path"
          :mode="imageMode"
        ></image>
        <!-- 加载中或已加载图片，绑定 load error 事件 -->
        <image
          v-else
          @tap.stop="doPreviewImage(item.url || item.path || item['data-url'], index)"
          class="preview-image"
          :src="item.url || item.path"
          :mode="imageMode"
          @load="onImgComplete(index)"
          @error="onImgComplete(index)"
        ></image>
      </block>
    </view>
    <slot name="file" :file="lists"></slot>
    <view style="display: inline-block" @tap="selectFile" v-if="maxCount > lists.length && !disabled">
      <slot name="addBtn"></slot>
      <view class="list-item-add add-wrap" :class="showBoder ? 'border' : ''" hover-class="add-wrap__hover" hover-stay-time="150">
        <template v-if="$slots.image">
          <slot name="image"></slot>
        </template>
        <!-- <ve-icon v-else name="add_btn" size="41" /> -->
        <image v-else class="add_btn" src="../../../static/images/upload.png" mode="widthFix"></image>
        <view class="u-add-tips" v-if="uploadText">{{ uploadText }}</view>
      </view>
    </view>
    <ve-toast ref="loadingRef"></ve-toast>
  </view>
</template>

<script>
  /**
   * upload 图片上传
   * @description 该组件用于上传图片场景
   * @tutorial https://www.uviewui.com/components/upload.html
   * @property {String Number} max-count 最大选择图片的数量（默认9）
   * @property {Boolean} show-progress 是否显示进度条（默认true）
   * @property {Boolean} disabled 是否启用(显示/移仓)组件（默认false）
   * @property {String | Number} index 在各个回调事件中的最后一个参数返回，用于区别是哪一个组件的事件
   * @property {Array<String>} size-type original 原图，compressed 压缩图，默认二者都有（默认['original', 'compressed']）
   * @property {Array<String>} source-type 选择图片的来源，album-从相册选图，camera-使用相机，默认二者都有（默认['album', 'camera']）
   * @property {Boolean} preview-full-image	是否可以通过uni.previewImage预览已选择的图片（默认true）
   * @property {Boolean} multiple	是否开启图片多选，部分安卓机型不支持（默认true）
   * @property {Boolean} deletable 是否显示删除图片的按钮（默认true）
   * @property {String Number} max-size 选择单个文件的最大大小，单位B(byte)，默认不限制（默认Number.MAX_VALUE）
   * @property {Array<Object>} file-list 默认显示的图片列表，数组元素为对象，必须提供url属性
   * @property {Boolean} upload-text 选择图片按钮的提示文字（默认“选择图片”）
   * @property {Boolean} auto-upload 选择完图片是否自动上传，见上方说明（默认true）
   * @property {Boolean} show-tips 特殊情况下是否自动提示toast，见上方说明（默认true）
   * @property {Boolean} show-upload-list 是否显示组件内部的图片预览（默认true）
   * @event {Function} on-oversize 图片大小超出最大允许大小
   * @event {Function} on-preview 全屏预览图片时触发
   * @event {Function} on-remove 移除图片时触发
   * @event {Function} on-success 图片上传成功时触发
   * @event {Function} on-change 图片上传后，无论成功或者失败都会触发
   * @event {Function} on-error 图片上传失败时触发
   * @event {Function} on-progress 图片上传过程中的进度变化过程触发
   * @event {Function} on-uploaded 所有图片上传完毕触发
   * @event {Function} on-choose-complete 每次选择图片后触发，只是让外部可以得知每次选择后，内部的文件列表
   * @example <ve-upload v-model="form.fileIds" />
   */
  import { getFile } from '@/api/common'
  // #ifdef APP-PLUS
  import permision from '@/utils/permissionCheck/permission.js'
  // #endif
  import { packagedUploadFile } from '@/utils/upload-tool'

  export default {
    name: 've-upload',
    props: {
      // 默认已上传的图片，这些图片没有 fieldStr 只有url （ota）
      defaultDisableUrls: {
        default: () => [],
        type: Array,
      },
      // 图片fileId集合
      modelValue: {
        default: () => [],
        type: [Array, String],
      },
      //是否显示组件自带的图片预览功能
      showUploadList: {
        type: Boolean,
        default: true,
      },
      // 最大上传数量
      maxCount: {
        type: [String, Number],
        default: 30,
      },
      //  是否显示进度条
      showProgress: {
        type: Boolean,
        default: true,
      },
      // 是否启用
      disabled: {
        type: Boolean,
        default: false,
      },
      // 预览上传的图片时的裁剪模式，和image组件mode属性一致
      imageMode: {
        type: String,
        default: 'aspectFill',
      },
      // 所选的图片的尺寸, 可选值为original compressed
      sizeType: {
        type: Array,
        default() {
          return ['original', 'compressed']
        },
      },
      sourceType: {
        type: Array,
        default() {
          return ['album', 'camera']
        },
      },
      // 是否在点击预览图后展示全屏图片预览
      previewFullImage: {
        type: Boolean,
        default: true,
      },
      // 是否开启图片多选，部分安卓机型不支持
      multiple: {
        type: Boolean,
        default: true,
      },
      // 是否展示删除按钮
      deletable: {
        type: Boolean,
        default: true,
      },
      // 文件大小限制，单位为byte
      maxSize: {
        type: [String, Number],
        default: Number.MAX_VALUE,
      },
      // 显示已上传的文件列表
      fileList: {
        type: Array,
        default() {
          return []
        },
      },
      // 上传区域的提示文字
      uploadText: {
        type: String,
        default: '',
      },
      // 是否自动上传
      autoUpload: {
        type: Boolean,
        default: true,
      },
      // 是否显示toast消息提示
      showTips: {
        type: Boolean,
        default: true,
      },
      // 如果上传后的返回值为json字符串，是否自动转json
      toJson: {
        type: Boolean,
        default: true,
      },
      // 上传前的钩子，每个文件上传前都会执行
      beforeUpload: {
        type: Function,
        default: null,
      },
      // 移除文件前的钩子
      beforeRemove: {
        type: Function,
        default: null,
      },
      // 允许上传的图片后缀
      limitType: {
        type: Array,
        default() {
          // 支付宝小程序真机选择图片的后缀为"image"
          // https://opendocs.alipay.com/mini/api/media-image
          return ['png', 'jpg', 'jpeg', 'webp', 'gif', 'image']
        },
      },
      // 在各个回调事件中的最后一个参数返回，用于区别是哪一个组件的事件
      index: {
        type: [Number, String],
        default: '',
      },
      // 是否显示border
      showBoder: {
        type: Boolean,
        default: true,
      },
      once: {
        type: Boolean,
        default: true,
      },
      single: {
        type: Boolean,
        default: false,
      },
    },
    mounted() {
      this.compareVersion()
    },
    data() {
      return {
        lists: [],
        uploading: false,
        version: undefined,
        osName: undefined,
      }
    },
    watch: {
      modelValue: {
        immediate: true,
        handler(val) {
          if (!val || val.length == 0) {
            if (!this.once) {
              this.lists = []
            }
            return false
          }
          if (this.lists.length && this.once) return
          this.getPicture()
        },
      },
      // 监听lists的变化，发出事件
      lists(n) {
        this.$emit('on-list-change', n)
      },
      uploading(val) {
        if (!this.$refs.loadingRef) return
        if (val) {
          this.$refs.loadingRef.showLoading({ title: '正在上传中...' })
        } else {
          this.$refs.loadingRef.hideLoading()
        }
      },
    },
    methods: {
      /**
       * 用于外部组件，更新当前文件信息，直接外部修改 modelValue 不会触发更新，
       * 因为子组件上传
       * @param ids 更新文件id，可能为单个id，也可能是数组
       */
      update(ids) {
        // 更新父组件id
        this.$emit('update:modelValue', ids)
        if (!ids || ids.length == 0) {
          // 清空
          this.lists = []
        } else {
          // 获取文件列表
          this.getPicture()
        }
      },
      handleUpload() {
        this.$emit('on-list-change', this.lists)
      },
      // 图片加载完成
      onImgComplete(index) {
        const curr = this.lists[index]
        // 新增图片不去触发 loadNextImg
        if (!curr.new) {
          this.loadNextImg(index + 1)
        }
      },
      loadNextImg(index = 0) {
        if (index >= this.lists.length) return
        const img = this.lists[index]
        if (img && img.waitFetching) {
          // 图片src赋值
          img.url = img['data-url']
          img.waitFetching = false
        }
      },
      // 获取照片
      getPicture() {
        // 兼容单张文件上传
        let idList = this.modelValue
        if (this.single) {
          idList = [idList]
        }
        getFile({
          idList,
        })
          .then((res) => {
            if (res.code == 200 && res.result.length) {
              this.lists = res.result.map((item, index) => {
                return {
                  url: '/static/images/img-loading.png',
                  'data-url': item.openUrl,
                  fileIdStr: item.fileIdStr,
                  error: false,
                  progress: 100,
                  waitFetching: true,
                }
              })
              this.handleUpload()
              this.loadNextImg()
            }
          })
          .catch((_) => {})
      },
      // 清除列表
      clear() {
        this.lists = []
      },
      // 重新上传队列中上传失败的所有文件
      reUpload() {
        this.uploadFile()
      },
      // 选择图片
      selectFile() {
        if (this.disabled) return
        const { name = '', maxCount, multiple, maxSize, sizeType, lists, camera, compressed, maxDuration, sourceType } = this
        let chooseFile = null
        const newMaxCount = maxCount - lists.length
        // 设置为只选择图片的时候使用 chooseImage 来实现
        if (this.osName == 'ios' && this.version == 1) {
          console.log('chooseMidia方法')
          chooseFile = new Promise((resolve, reject) => {
            uni.chooseMedia({
              count: multiple ? (newMaxCount > 20 ? 20 : newMaxCount) : 1,
              mediaType: ['image'],
              sourceType: sourceType,
              sizeType,
              success: resolve,
              fail: reject,
            })
          })
        } else {
          chooseFile = new Promise((resolve, reject) => {
            uni.chooseImage({
              count: multiple ? (newMaxCount > 9 ? 9 : newMaxCount) : 1,
              sourceType: sourceType,
              sizeType,
              success: resolve,
              fail: reject,
            })
          })
        }
        // #ifdef APP-PLUS
        permision.premissionCheck('CAMERA_EXTERNAL_STORAGE').then((result1) => {
          console.log('result1', result1)
          if (result1 == 1) {
            // #endif
            chooseFile
              .then((res) => {
                // console.log('res', res)
                let file = null
                let listOldLength = this.lists.length
                res.tempFiles.map((val, index) => {
                  // 检查文件后缀是否允许，如果不在this.limitType内，就会返回false
                  // if (!this.checkFileExt(val)) return;

                  // 如果是非多选，index大于等于1或者超出最大限制数量时，不处理
                  if (!multiple && index >= 1) return
                  if (val.size > maxSize) {
                    this.$emit('on-oversize', val, this.lists, this.index)
                    this.showToast('超出允许的文件大小')
                  } else {
                    if (maxCount <= lists.length) {
                      this.$emit('on-exceed', val, this.lists, this.index)
                      this.showToast('超出最大允许的文件个数')
                      return
                    }
                    lists.push({
                      url: val.path ? val.path : val.tempFilePath,
                      progress: 0,
                      error: false,
                      file: val,
                      fileIdStr: '',
                    })
                  }
                })
                // 每次图片选择完，抛出一个事件，并将当前内部选择的图片数组抛出去
                this.$emit('on-choose-complete', this.lists, this.index)
                if (this.autoUpload) this.uploadFile(listOldLength)
              })
              .catch((error) => {
                this.$emit('on-choose-fail', error)
              })
            // #ifdef APP-PLUS
          }
        })
        // #endif
      },
      // 提示用户消息
      showToast(message, force = false) {
        if (this.showTips || force) {
          uni.showToast({
            title: message,
            icon: 'none',
          })
        }
      },
      // 该方法供用户通过ref调用，手动上传
      upload() {
        this.uploadFile()
      },
      // 对失败的图片重新上传
      retry(index) {
        this.lists[index].progress = 0
        this.lists[index].error = false
        this.lists[index].fileIdStr = null
        uni.showLoading({
          title: '重新上传',
        })
        this.uploadFile(index)
      },
      // 上传图片
      async uploadFile(index = 0) {
        if (this.disabled) return
        if (this.uploading) return
        // 全部上传完成
        if (index >= this.lists.length) {
          this.$emit('on-uploaded', this.lists, this.index)
          return
        }
        // 检查是否是已上传或者正在上传中
        if (this.lists[index].progress == 100) {
          if (this.autoUpload == false) this.uploadFile(index + 1)
          return
        }
        // 执行before-upload钩子
        if (this.beforeUpload && typeof this.beforeUpload === 'function') {
          // 执行回调，同时传入索引和文件列表当作参数
          // 在微信，支付宝等环境(H5正常)，会导致父组件定义的customBack()函数体中的this变成子组件的this
          // 通过bind()方法，绑定父组件的this，让this.customBack()的this为父组件的上下文
          // 因为upload组件可能会被嵌套在其他组件内，比如form，这时this.$parent其实为form的this，
          // 非页面的this，所以这里需要往上历遍，一直寻找到最顶端的$parent，这里用了this.$u.$parent.call(this)
          // 明白意思即可，无需纠结this.$u.$parent.call(this)的细节
          let beforeResponse = this.beforeUpload.bind(this.$parent.call(this))(index, this.lists)
          // 判断是否返回了promise
          if (!!beforeResponse && typeof beforeResponse.then === 'function') {
            await beforeResponse
              .then((res) => {
                // promise返回成功，不进行动作，继续上传
              })
              .catch((err) => {
                // 进入catch回调的话，继续下一张
                return this.uploadFile(index + 1)
              })
          } else if (beforeResponse === false) {
            // 如果返回false，继续下一张图片的上传
            return this.uploadFile(index + 1)
          } else {
            // 此处为返回"true"的情形，这里不写代码，就跳过此处，继续执行当前的上传逻辑
          }
        }
        this.lists[index].error = false
        this.lists[index].new = true
        this.uploading = true
        // 创建上传对象
        console.log(getApp().globalData)
        const task = packagedUploadFile({
          filePath: this.lists[index].url,
          success: (res) => {
            // 判断是否json字符串，将其转为json格式
            // let data = this.toJson && this.$u.test.jsonString(res.data) ? JSON.parse(res.data) : res.data;
            let data = JSON.parse(res.data)
            // console.log(data)
            if (data.code == 200) {
              // 上传成功
              this.lists[index].fileIdStr = data.result[0].fileIdStr
              this.lists[index].progress = 100
              this.lists[index].error = false
              const ids = this.lists.map((it) => it.fileIdStr)
              // 兼容单张图片上传
              if (this.single) {
                this.$emit('update:modelValue', ids[0])
              } else {
                this.$emit('update:modelValue', ids)
              }
              this.$emit('on-success', data, index, this.lists, this.index)
              this.$emit('on-list-change', this.lists) //重要更新 监听
            } else {
              this.uploadError(index, data)
            }
          },
          fail: (e) => {
            this.uploadError(index, e)
          },
          complete: (res) => {
            uni.hideLoading()
            this.uploading = false
            this.uploadFile(index + 1)
            this.$emit('on-change', res, index, this.lists, this.index)
          },
        })
        task.onProgressUpdate((res) => {
          if (res.progress > 0) {
            this.lists[index].progress = res.progress
            this.$emit('on-progress', res, index, this.lists, this.index)
          }
        })
      },
      // 上传失败
      uploadError(index, err) {
        this.lists[index].progress = 0
        this.lists[index].error = true
        this.lists[index].fileIdStr = null
        this.$emit('on-error', err, index, this.lists, this.index)
        this.showToast('上传失败，请重试')
      },
      // 删除一个图片
      deleteItem(index) {
        uni.showModal({
          title: '提示',
          content: '您确定要删除此项吗？',
          confirmText: '确认',
          success: async (res) => {
            if (res.confirm) {
              // 先检查是否有定义before-remove移除前钩子
              // 执行before-remove钩子
              if (this.beforeRemove && typeof this.beforeRemove === 'function') {
                // 此处钩子执行 原理同before-remove参数，见上方注释
                let beforeResponse = this.beforeRemove.bind(this.$u.$parent.call(this))(index, this.lists)
                // 判断是否返回了promise
                if (!!beforeResponse && typeof beforeResponse.then === 'function') {
                  await beforeResponse
                    .then((res) => {
                      // promise返回成功，不进行动作，继续上传
                      this.handlerDeleteItem(index)
                    })
                    .catch((err) => {
                      // 如果进入promise的reject，终止删除操作
                      this.showToast('已终止移除')
                    })
                } else if (beforeResponse === false) {
                  // 返回false，终止删除
                  this.showToast('已终止移除')
                } else {
                  // 如果返回true，执行删除操作
                  this.handlerDeleteItem(index)
                }
              } else {
                // 如果不存在before-remove钩子，
                this.handlerDeleteItem(index)
              }
            }
          },
        })
      },
      // 执行移除图片的动作，上方代码只是判断是否可以移除
      handlerDeleteItem(index) {
        // 如果文件正在上传中，终止上传任务，进度在0 < progress < 100则意味着正在上传
        if (this.lists[index].process < 100 && this.lists[index].process > 0) {
          typeof this.lists[index].uploadTask != 'undefined' && this.lists[index].uploadTask.abort()
        }
        this.lists.splice(index, 1)
        this.$forceUpdate()
        this.$emit('on-remove', index, this.lists, this.index)
        this.showToast('移除成功')
        // 更新value
        const ids = this.lists.map((it) => it.fileIdStr)
        // 兼容单张图片上传
        if (this.single) {
          this.$emit('update:modelValue', ids[0])
          return
        }
        this.$emit('update:modelValue', ids)
      },
      // 用户通过ref手动的形式，移除一张图片
      remove(index) {
        // 判断索引的合法范围
        if (index >= 0 && index < this.lists.length) {
          this.lists.splice(index, 1)
          this.$emit('on-list-change', this.lists, this.index)
        }
      },
      // 预览图片
      doPreviewImage(url, index) {
        if (!this.previewFullImage) return
        const images = this.lists.map((item) => item.url || item.path || item.tempFilePath)
        uni.previewImage({
          urls: images,
          current: url,
          success: () => {
            this.$emit('on-preview', url, this.lists, this.index)
          },
          fail: () => {
            uni.showToast({
              title: '预览图片失败',
              icon: 'none',
            })
          },
        })
      },
      // 判断文件后缀是否允许
      checkFileExt(file) {
        // 检查是否在允许的后缀中
        let noArrowExt = false
        // 获取后缀名
        let fileExt = ''
        const reg = /.+\./
        // 如果是H5，需要从name中判断
        // #ifdef H5
        fileExt = file.name.replace(reg, '').toLowerCase()
        // #endif
        // 非H5，需要从path中读取后缀
        // #ifndef H5
        fileExt = this.osName == 'ios' ? file.tempFilePath.replace(reg, '').toLowerCase() : file.path.replace(reg, '').toLowerCase()
        // #endif
        // 使用数组的some方法，只要符合limitType中的一个，就返回true
        noArrowExt = this.limitType.some((ext) => {
          // 转为小写
          return ext.toLowerCase() === fileExt
        })
        if (!noArrowExt) this.showToast(`不允许选择${fileExt}格式的文件`)
        return noArrowExt
      },
      // 微信基础库版本判断   compareVersion('1.11.0', '2.11.1') // 1
      compareVersion() {
        var _this = this
        var v1 = ''
        var v2 = '2.25.0'
        uni.getSystemInfo({
          success(res) {
            console.log(res)
            if (res.osName) {
              _this.osName = res.osName
            }
            if (res.SDKVersion) {
              v1 = res.SDKVersion.split('.')
              v2 = v2.split('.')
              const len = Math.max(v1.length, v2.length)
              while (v1.length < len) {
                v1.push('0')
              }
              while (v2.length < len) {
                v2.push('0')
              }
              for (let i = 0; i < len; i++) {
                const num1 = parseInt(v1[i])
                const num2 = parseInt(v2[i])
                if (num1 > num2) {
                  _this.version = 1
                } else if (num1 < num2) {
                  _this.version = -1
                }
              }
            }
          },
        })
      },
    },
  }
</script>

<style lang="scss" scoped>
  .upload {
    // margin: 20rpx 0;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 20rpx;
    .empty-block {
      padding-top: 52rpx;
      text-align: center;
      width: 204rpx;
      height: 204rpx;
      background: #f2f3f5;
      border-radius: 4rpx 4rpx 4rpx 4rpx;
      border: 1px solid #e2e2e2;
      // 字
      font-size: 24rpx;
      font-weight: 400;
      color: #b6b6b6;
      line-height: 40rpx;
    }
  }
  .add_btn {
    display: inline-block;
    width: 41rpx;
  }
  .list-item {
    min-width: 204rpx;
    height: 204rpx;
    overflow: hidden;
    // background: rgb(244, 245, 246);
    border-radius: 8rpx;
    border: 2rpx dashed #e2e2e2;
    overflow: hidden;
    position: relative;
    border-radius: 4rpx;
    /* #ifndef APP-NVUE */
    display: flex;
    /* #endif */
    align-items: center;
    justify-content: center;
  }

  .list-item-add {
    min-width: 204rpx;
    height: 204rpx;
    overflow: hidden;
    // background: rgb(244, 245, 246);
    border-radius: 8rpx;
    overflow: hidden;
    position: relative;
    border-radius: 4rpx;
    /* #ifndef APP-NVUE */
    display: flex;
    /* #endif */
    align-items: center;
    justify-content: center;
  }

  .border {
    border: 2rpx dashed #9ea5bb;
  }

  .list-item-image:nth-child(3n) {
    margin-right: 0;
  }

  .preview-wrap {
    border: 1px solid rgb(235, 236, 238);
  }

  .add-wrap {
    flex-direction: column;
    // color: $content-color;
    font-size: 26rpx;

    .add-btn {
      width: 44rpx;
      height: 44rpx;
    }
  }

  .add-tips {
    margin-top: 20rpx;
    line-height: 40rpx;
  }

  .add-wrap__hover {
    background-color: rgb(235, 236, 238);
  }

  .preview-image {
    display: block;
    width: 100%;
    height: 100%;
    border-radius: 4rpx;
  }

  .delete-icon {
    position: absolute;
    top: 10rpx;
    right: 0rpx;
    z-index: 1;
    border-radius: 100rpx;
    width: 44rpx;
    height: 44rpx;
    display: flex;
    align-items: center;
    justify-content: center;
  }

  .icon {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 44rpx;
    height: 44rpx;
  }

  .progress {
    position: absolute;
    bottom: 10rpx;
    left: 8rpx;
    right: 8rpx;
    z-index: 1;
    width: auto;
  }

  .error-btn {
    background: rgba(0, 0, 0, 0.5);
    font-size: 20rpx;
    padding: 4px 0;
    font-size: 24rpx;
    font-weight: 400;
    color: #ffffff;
    line-height: 28rpx;
    text-align: center;
    position: absolute;
    bottom: 0;
    left: 0;
    right: 0;
    z-index: 9;
    line-height: 1;
  }
  .icon-clear-img {
    position: absolute;
    right: 10rpx;
    color: rgba(0, 0, 0, 0.8);
    width: 48rpx;
    height: 48rpx;
  }

  .u-add-tips {
    margin-top: 20rpx;
    line-height: 40rpx;
    font-size: 20rpx;
  }
</style>
